---
title: metro.config.js
description: A reference of available configurations in Metro.
---

import { FileTree } from '~/ui/components/FileTree';
import { Terminal } from '~/ui/components/Snippet';

See more information about **metro.config.js** in the [customizing Metro guide](/guides/customizing-metro/).

## Environment variables

Environment variables can be loaded using **.env** files. These files are loaded according to the [standard **.env** file resolution](https://github.com/bkeepers/dotenv/blob/c6e583a/README.md#what-other-env-files-can-i-use).

If you are migrating an older project to SDK 49 or above, then you should ignore local env files by adding the following to your **.gitignore**:

```sh .gitignore
# local env files
.env*.local
```

> Dotenv files are Expo CLI-specific as of SDK 49. EAS CLI uses a different mechanism for environment variables until it invokes Expo CLI for compiling and bundling. Learn more about [environment variables in EAS](/build-reference/variables/).

### Disabling dotenv files

Dotenv file loading can be fully disabled in Expo CLI by enabling the `EXPO_NO_DOTENV` environment variable, before invoking any Expo CLI command.

<Terminal
  cmd={[
    '# All users can run cross-env, followed by the Expo CLI command',
    '$ npx cross-env EXPO_NO_DOTENV=1 expo start',
    '# Alternatively, macOS users can define the environment variable, then run npx, followed by the Expo CLI command',
    '$ EXPO_NO_DOTENV=1 npx expo start',
  ]}
/>

### Client environment variables

Environment variables prefixed with `EXPO_PUBLIC_` will be exposed to the app at build-time. For example, `EXPO_PUBLIC_API_KEY` will be available as `process.env.EXPO_PUBLIC_API_KEY`.

Environment variables will not be inlined in code inside of **node_modules**.

For security purposes, client environment variables are inlined in the bundle, which means that `process.env` is not an iterable object, and you cannot dynamically access environment variables. Every variable must be referenced as a static property for it to be inlined. For example, the expression `process.env.EXPO_PUBLIC_KEY` will be rewritten and `process.env['EXPO_PUBLIC_KEY']` will not.

- Client environment variables should not contain secrets as they will be viewable in plain-text format in the production binary.
- Use client environment variables for partially protected values, such as public API keys you don't want to commit to Git or hard-code in your app and that may change depending on the environment.
- Expo environment variables can be updated while the development server (`npx expo start`) is running, without restarting or clearing the bundler cache — however, you'll need to modify and save an included source file to see the updates. You must also perform a client reload, as environment variables do not support Fast Refresh.
- Client environment variable inlining can be disabled with the environment variable `EXPO_NO_CLIENT_ENV_VARS=1`, this must be defined before any bundling is performed.

{/* TODO: Usage in EAS */}

## CSS

> Available in SDK 49 and higher.

> **info** CSS support is under development and currently only works on web.

Expo supports CSS in your project. You can import CSS files from any component. CSS Modules are also supported. To enable CSS, configure your **metro.config.js** as follows, setting `isCSSEnabled` to `true`:

```js metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
  // Enable CSS support.
  isCSSEnabled: true,
});

module.exports = config;
```

Now you'll need to clear the Metro cache and restart the development server:

<Terminal cmd={['$ npx expo start --clear']} />

Ensure you don't have a custom Metro transformer that processes CSS files. If you do, you'll need to remove it.

### Global CSS

> **warning** Global styles are web-only, usage will cause your application to diverge visually on native.

You can import a CSS file from any component. The CSS will be applied to the entire page.

Here, we'll define a global style for the class name `.container`:

```css styles.css
.container {
  background-color: red;
}
```

We can then use the class name in our component by importing the stylesheet and using `.container`:

```jsx App.js
import './styles.css';
import { View } from 'react-native';

export default function App() {
  return (
    <>
      {/* Use `className` to assign the style with React DOM components. */}
      <div className="container">Hello World</div>

      {/* Use `style` with the following syntax to append class names in React Native for web. */}
      <View
        style={{
          $$css: true,
          _: 'container',
        }}>
        Hello World
      </View>
    </>
  );
}
```

You can also import stylesheets that are vendored in libraries, just like you would any node module:

```js index.js
// Applies the styles app-wide.
import 'emoji-mart/css/emoji-mart.css';
```

- On native, all global stylesheets are automatically ignored.
- Hot reloading is supported for global stylesheets, simply save the file and the changes will be applied.

### CSS Modules

> **warning** CSS Modules for native are under development and currently only work on web.

CSS Modules are a way to scope CSS to a specific component. This is useful for avoiding naming collisions and for ensuring that styles are only applied to the intended component.

In Expo, CSS Modules are defined by creating a file with the `.module.css` extension. The file can be imported from any component. The exported value is an object with the class names as keys and the web-only scoped names as the values. The import `unstable_styles` can be used to access `react-native-web`-safe styles.

CSS Modules support platform extensions to allow you to define different styles for different platforms. For example, you can define a `module.ios.css` and `module.android.css` file to define styles for Android and iOS respectively. You'll need to import without the extension, for example:

```diff App.js
// Importing `./App.module.ios.css`:
- import styles from './App.module.css';
+ import styles from './App.module';
```

Flipping the extension, for example, `App.ios.module.css` will not work and result in a universal module named `App.ios.module`.

> You cannot pass styles to the `className` prop of a React Native or React Native for web component. Instead, you must use the `style` prop.

```jsx App.js
import styles, { unstable_styles } from './App.module.css';

export default function Page() {
  return (
    <>
      <Text
        style={{
          // This is how react-native-web class names are applied
          $$css: true,
          _: styles.text,
        }}>
        Hello World
      </Text>
      <Text style={unstable_styles.text}>Hello World</Text>
      {/* Web-only usage: */}
      <p className={styles.text}>Hello World</p>
    </>
  );
}
```

```css App.module.css
.text {
  color: red;
}
```

- On web, all CSS values are available. CSS is not processed or auto-prefixed like it is with the React Native Web `StyleSheet` API. You can use `postcss.config.js` to autoprefix your CSS.
- CSS Modules use [lightningcss](https://github.com/parcel-bundler/lightningcss) under the hood, check [the issues](https://github.com/parcel-bundler/lightningcss/issues) for unsupported features.

### PostCSS

> Changing the Post CSS or `browserslist` config will require you to clear the Metro cache: `npx expo start --clear` | `npx expo export --clear`.

[PostCSS](https://github.com/postcss/postcss) can be customized by adding a `postcss.config.json` file to the root of your project. This file should export a function that returns a PostCSS configuration object. For example:

```json postcss.config.json
{
  "plugins": {
    "autoprefixer": {}
  }
}
```

Both `postcss.config.json` and `postcss.config.js` are supported, but `postcss.config.json` enables better caching.

### SASS

Expo Metro has _partial_ support for SCSS/SASS.

To setup, install the `sass` package in your project:

<Terminal cmd={['$ yarn add -D sass']} />

Then, ensure [CSS is setup](#css) in the **metro.config.js** file.

- When `sass` is installed, then modules without extensions will be resolved in the following order: `scss`, `sass`, `css`.
- Only use the intended syntax with `sass` files.
- Importing other files from inside a scss/sass file is not currently supported.

### Tailwind

> **info** Tailwind does not support native platforms, this is web-only.

[Tailwind](https://tailwindcss.com/) can be used with Metro for web. However, due to the advanced caching system in Metro, the setup is a little different from the default Tailwind setup. The following files are modified:

<FileTree
  files={[
    'app.json',
    'package.json',
    'global.css',
    'metro.config.js',
    'tailwind.config.js',
    'index.js',
  ]}
/>

To setup, install the `tailwindcss` package in your project:

<Terminal cmd={['$ yarn add -D tailwindcss@^3.3.1']} />

In your **app.json** ensure the project is using Metro for web:

```json app.json
{
  "expo": {
    "web": {
      "bundler": "metro"
    }
  }
}
```

Create **global.css** in your project:

```css global.css
/* This file adds the requisite utility classes for Tailwind to work. */
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Create **tailwind.config.js** in your project:

```js tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    // Ensure this points to your source code...
    './app/**/*.{js,tsx,ts,jsx}',
    // If you use a `src` folder, add: './src/**/*.{js,tsx,ts,jsx}'
    // Do the same with `components`, `hooks`, `styles`, or any other top-level folders...
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

In **metro.config.js**, enable CSS support and run the Tailwind CLI:

```js metro.config.js
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const tailwind = require('tailwindcss/lib/cli/build');

module.exports = (async () => {
  /** @type {import('expo/metro-config').MetroConfig} */
  const config = getDefaultConfig(__dirname, {
    // Enable CSS support.
    isCSSEnabled: true,
  });

  // Run Tailwind CLI to generate CSS files.
  await tailwind.build({
    '--input': path.relative(__dirname, './global.css'),
    '--output': path.resolve(__dirname, 'node_modules/.cache/expo/tailwind/eval.css'),
    '--watch': process.env.NODE_ENV === 'development' ? 'always' : false,
    '--poll': true,
  });

  return config;
})();
```

In your app main entry file, add the following:

```js index.js/App.js
// Ensure we import the CSS for Tailwind so it's included in hot module reloads.
const ctx = require.context(
  // If this require.context is not inside the root directory (next to the package.json) then adjust this file path
  // to resolve correctly.
  './node_modules/.cache/expo/tailwind'
);
if (ctx.keys().length) ctx(ctx.keys()[0]);
```

#### Tailwind usage

You can use Tailwind with React DOM elements as-is:

```jsx
export default function Page() {
  return (
    <div className="bg-slate-100 rounded-xl">
      <p className="text-lg font-medium">Welcome to Tailwind</p>
    </div>
  );
}
```

You can use the `{ $$css: true }` syntax to use Tailwind with React Native web elements:

```jsx
import { View, Text } from 'react-native';

export default function Page() {
  return (
    <View style={{ $$css: true, _: 'bg-slate-100 rounded-xl' }}>
      <Text style={{ $$css: true, _: 'text-lg font-medium' }}>Welcome to Tailwind</Text>
    </View>
  );
}
```

## Bare workflow setup

> This guide is versioned and will need to be revisited when upgrading/downgrading Expo. Alternatively, use [Expo Prebuild](/workflow/prebuild) for fully automated setup.

Projects that don't use [Expo Prebuild](/workflow/prebuild) must configure native files to ensure the Expo Metro config is always used to bundle the project.

{/* If this isn't done, then features like [aliases](/guides/typescript/#path-aliases-optional), [absolute imports](/guides/typescript/#absolute-imports-optional), asset hashing, and more will not work. */}

These modifications are meant to replace `npx react-native bundle` and `npx react-native start` with `npx expo export:embed` and `npx expo start` respectively.

### metro.config.js

Ensure the **metro.config.js** extends `expo/metro-config`:

```js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

module.exports = config;
```

### `android/app/build.gradle`

The Android `app/build.gradle` must be configured to use Expo CLI for production bundling. Modify the `react` config object:

```diff
react {
  ...
+     // Use Expo CLI to bundle the app, this ensures the Metro config
+     // works correctly with Expo projects.
+     cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
+     bundleCommand = "export:embed"
}
```

### `ios/<Project>.xcodeproj/project.pbxproj`

In your `ios/<Project>.xcodeproj/project.pbxproj` file, replace the following scripts:

#### `"Start Packager"`

```diff
+			shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `$NODE_BINARY --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n  if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n    if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n      echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n      exit 2\n    fi\n  else\n    open `$NODE_BINARY --print \"require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n  fi\nfi\n";
```

**Alternatively**, in the Xcode project, select the "Start Packager" build phase and add the following modifications:

```diff
+ if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then
+   source "$PODS_ROOT/../.xcode.env"
+ fi
+ if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
+   source "$PODS_ROOT/../.xcode.env.local"
+ fi

export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > `$NODE_BINARY --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'"`
if [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ] ; then
  if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
    if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running" ; then
      echo "Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly"
      exit 2
    fi
  else
-     open `$NODE_BINARY --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/launchPackager.command'"` || echo "Can't start packager automatically"
+     open `$NODE_BINARY --print "require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'"` || echo "Can't start packager automatically"
  fi
fi
```

#### "Bundle React Native code and images"

```diff
+			shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n  export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n  # Set the entry JS file using the bundler's entry resolution.\n  export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n  # Use Expo CLI\n  export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n  # Default Expo CLI command for bundling\n  export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
```

**Alternatively**, in the Xcode project, select the "Bundle React Native code and images" build phase and add the following modifications:

```diff
if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then
  source "$PODS_ROOT/../.xcode.env"
fi
if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
  source "$PODS_ROOT/../.xcode.env.local"
fi

# The project root by default is one level up from the ios directory
export PROJECT_ROOT="$PROJECT_DIR"/..

if [[ "$CONFIGURATION" = *Debug* ]]; then
  export SKIP_BUNDLING=1
fi
+ if [[ -z "$ENTRY_FILE" ]]; then
+   # Set the entry JS file using the bundler's entry resolution.
+   export ENTRY_FILE="$("$NODE_BINARY" -e "require('expo/scripts/resolveAppEntry')" "$PROJECT_ROOT" ios absolute | tail -n 1)"
+ fi

+ if [[ -z "$CLI_PATH" ]]; then
+   # Use Expo CLI
+   export CLI_PATH="$("$NODE_BINARY" --print "require.resolve('@expo/cli')")"
+ fi
+ if [[ -z "$BUNDLE_COMMAND" ]]; then
+   # Default Expo CLI command for bundling
+   export BUNDLE_COMMAND="export:embed"
+ fi

`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"`
```

> You can set `CLI_PATH`, `BUNDLE_COMMAND`, and `ENTRY_FILE` environment variables to overwrite these defaults.

### Custom entry file

By default, React Native only supports using a root `index.js` file as the entry file (or platform-specific variation like `index.ios.js`). Expo projects allow using any entry file, but this requires addition bare setup.

#### Development

Development mode entry files can be enabled by using the [`expo-dev-client`](/versions/latest/sdk/dev-client/) package. Alternatively you can add the following configuration:

In the `ios/[project]/AppDelegate.mm` file:

```diff
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
-  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
+  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
```

In the `android/app/src/main/java/**/MainApplication.java`:

```diff
@Override
protected String getJSMainModuleName() {
-  return "index";
+  return ".expo/.virtual-metro-entry";
}
```

#### Production

In your `ios/<Project>.xcodeproj/project.pbxproj` file, replace the `"Bundle React Native code and images"` script to set `$ENTRY_FILE` according using Metro:

```diff
+			shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n  export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n  # Set the entry JS file using the bundler's entry resolution.\n  export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n  # Use Expo CLI\n  export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n  # Default Expo CLI command for bundling\n  export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
```

The Android `app/build.gradle` must be configured to use Metro module resolution to find the root entry file. Modify the `react` config object:

```diff
+ def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()

react {
+    entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
}
```
